--[[
Name: InventoryLib-2.0
Revision: $Rev: 2 $
Author(s): Elkano (elkano@gmx.de)
Website: http://elkano.wowinterface.com/
Documentation: http://wiki.wowace.com/index.php/InventoryLib-2.0
SVN: http://svn.wowace.com/root/trunk/InventoryLib/InventoryLib-2.0
Description: A library that maintains a cache of linkable items and shares it between clients.
Dependencies: AceLibrary, AceEvent-2.0, Gratuity-2.0
]]

local MAJOR_VERSION = "InventoryLib-2.0"
local MINOR_VERSION = "$Revision: 2 $"

if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if not AceLibrary:HasInstance("AceEvent-2.0") then error(MAJOR_VERSION .. " requires AceEvent-2.0") end

local InventoryLib = {}
-- -----
-- public functions
-- -----

-- returns a table of all itemIDs in cache
-- the list is rescanned automatically every 5 min on call or if forcedRescan is set
function InventoryLib:GetItems(forceRescan)
	if forceRescan or time() > self.localvars.items_all_lastrescan + 300 then
		self:BuildLists()
	end
	return self.localvars.items
end

-- returns true if the item from link is in the client's cache
function InventoryLib:IsCacheItem(link)
	local _, _, itemID = string.find(link, "item:(%d+):%d+:%d+:%d+")
	if not itemID then itemID = link end
	if GetItemInfo(itemID) then
		return true
	else
		return false
	end
end

-- returns true if the item from link is known as valid for this session
function InventoryLib:IsSessionItem(link)
end

-- -----
-- internal functions
-- -----

-- this function scans the client's cache for items
-- it will also build a tree for the various types an item can have and sortet lists for the types and subtypes
function InventoryLib:BuildLists(forceRescan, scanid)
	self:RecheckSessionItems()

	local newtype = false
	local first, last
	if forceRescan then
		first = 1
		last = self.consts.maxid
		self.localvars.items_scan_last = 0
	else
		if scanid then
			first = scanid
			last = scanid
		else
			first = self.localvars.items_scan_last + 1
			last = self.localvars.items_scan_last + self.consts.itemscancount
			self.localvars.items_scan_last = (last < self.consts.maxid) and last or 0
		end
	end
	for i = first, last do
		local itemName, itemLink, itemRarity, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc = GetItemInfo(i)
		if ( itemName ) then	-- valid item
			itemEquipLoc = getglobal(itemEquipLoc)
			if (not itemEquipLoc or itemEquipLoc == "") then
				itemEquipLoc = self.locals.INVTYPE_OTHER
			end
			if not self.localvars.items[i] then
				self.localvars.items[i] = false
			end
			if (not self.localvars.types_tree[itemType]) then
				self.localvars.types_tree[itemType] = {}
				newtype = true
			end
			if (not self.localvars.types_tree[itemType][itemSubType]) then
				self.localvars.types_tree[itemType][itemSubType] = {}
				newtype = true
			end
			if (not self.localvars.types_tree[itemType][itemSubType][itemEquipLoc]) then
				self.localvars.types_tree[itemType][itemSubType][itemEquipLoc] = ""
				newtype = true
			end
		end
	end
	if not newtype then return end
	self.localvars.types_base = {}
	self.localvars.types_sub = {}
	self.localvars.types_slot = {}
	for type, subtypes in pairs(self.localvars.types_tree) do
		table.insert(self.localvars.types_base, type)
		local subtypesort = {}
		local hasinvtypes = false
		for subtype, invtypes in pairs(subtypes) do
			table.insert(subtypesort, subtype)
			local invtypesort = {}
			for invtype, _ in pairs(invtypes) do
				table.insert(invtypesort, invtype)
			end
			if (table.getn(invtypesort) > 1) then
				table.sort(invtypesort)
				if (not self.localvars.types_slot[type]) then
					self.localvars.types_slot[type] = {}
				end
				self.localvars.types_slot[type][subtype] = invtypesort
				hasinvtypes = true
			end
		end
		if (table.getn(subtypesort) > 1 or hasinvtypes) then
			table.sort(subtypesort);
			self.localvars.types_sub[type] = subtypesort
		end
	end
	table.sort(self.localvars.types_base)
end

function InventoryLib:StoreSessionItem(itemID)
	baseID = self:link2baseid(itemID)
	if not baseID then return false end
	if (not self.localvars.items[baseID]) then
		AceLibrary("Gratuity-2.0"):SetHyperlink(itemID)
		local itemName, itemLink, itemRarity, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc = GetItemInfo(baseID)
		if ( itemName ) then	-- valid item
			itemEquipLoc = getglobal(itemEquipLoc)
			if (not itemEquipLoc or itemEquipLoc == "") then
				itemEquipLoc = ITEMDB_STRING_INVTYPE_OTHER
			end
			self.localvars.items[baseID] = true
			if not (self.localvars.types_tree and self.localvars.types_tree[itemType] and self.localvars.types_tree[itemType][itemSubType] and self.localvars.types_tree[itemType][itemSubType][itemEquipLoc]) then
				self:BuildLists(true)
			end
			return true
		else
			self.localvars.items_rescan[baseID] = ""
		end
	end
	return false
end

function InventoryLib:RecheckSessionItems()
	for baseid, _ in self.localvars.items_rescan do
		if (ItemDB_DBFrame_StoreSessionItem("item:"..baseid..":0:0:0")) then
			self.localvars.items_rescan[baseid] = nil;
		end
	end
end

-- -----
-- the following functions scan various sources for items in order to add them to the list of linkable items for this session
-- -----

-- if given unit hasn't been inspected lately all slots will be scaned for items
function InventoryLib:Inspect(unit)
	if not (UnitExists(unit) and UnitIsPlayer(unit) and UnitIsFriend(unit, "player") and CheckInteractDistance(unit, 1)) then
		return
	end
	local timenow = time()
	if (timenow - self.localvars.inspected_lastclean > 5) then -- clean up cache at most every 5 seconds
		for key, value in self.localvars.inspected_units do
			if (timenow - value > 300) then -- remove after 5min
				self.localvars.inspected_units[key] = NIL
			end
		end
		self.localvars.inspected_lastclean = timenow
	end
	local name, server = UnitName(unit)
	if not server then server = self.consts.RealmName end
	if (self.localvars.inspected_units[name.."-"..server]) then
		return
	end
	self.localvars.inspected_units[name.."-"..server] = timenow
	for i=1, table.getn(self.consts.InventorySlotName) do
		local itemlink = GetInventoryItemLink(unit, GetInventorySlotInfo(self.consts.InventorySlotName[i]))
		if (itemlink) then
			local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
			self:StoreSessionItem(itemID)
		end
	end
end

function InventoryLib:ScanAuctionPage()
	local numBatchAuctions = GetNumAuctionItems("list")
	for auctionid = 1, numBatchAuctions do
		local itemlink = GetAuctionItemLink("list", auctionid)
		if (itemlink) then
			local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
			self:StoreSessionItem(itemID)
		end
	end
end

function InventoryLib:ScanBags()
	for bagid = 0, NUM_BAG_SLOTS do
		local size = GetContainerNumSlots(bagid)
		if (size) then
			for slotid = 1, size do
				local itemlink = GetContainerItemLink(bagid, slotid)
				if (itemlink) then
					local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
					self:StoreSessionItem(itemID)
				end
			end
		end
	end
end

function InventoryLib:ScanBank()
	for bagid_i = NUM_BAG_SLOTS, (NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) do
		local bagid = bagid_i
		if (bagid == NUM_BAG_SLOTS) then
			bagid = BANK_CONTAINER
		end
		local size = GetContainerNumSlots(bagid)
		if (size) then
			for slotid = 1, size do
				local itemlink = GetContainerItemLink(bagid, slotid)
				if (itemlink) then
					local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
					self:StoreSessionItem(itemID)
				end
			end
		end
	end
end

function InventoryLib:ScanMerchant()
	if (MerchantFrame:IsVisible() and MerchantFrame.selectedTab == 1 and (time() > self.localvars.framescan_last + 1)) then
		-- This prevents further update events from being handled if we're already processing one.
		-- This is done to prevent the game from freezing under certain conditions.
		self.localvars.framescan_last = time();
		local numMerchantItems = GetMerchantNumItems()
		for i = 1, numMerchantItems do
			local itemlink = GetMerchantItemLink(i)
			if (itemlink) then
				local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
				self:StoreSessionItem(itemID)
			end
		end
	end
end

function InventoryLib:ScanTradeSkill()
	if (TradeSkillFrame ~= nil and TradeSkillFrame:IsVisible() and (time() > self.localvars.framescan_last + 1)) then
		-- This prevents further update events from being handled if we're already processing one.
		-- This is done to prevent the game from freezing under certain conditions.
		self.localvars.framescan_last = time()
		for i=1, GetNumTradeSkills() do
			local _, type, _, _ = GetTradeSkillInfo(i)
			if (type ~= "header") then
				local itemlink = GetTradeSkillItemLink(i)
				if (itemlink) then
					local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
					self:StoreSessionItem(itemID)
				end
				for j=1, GetTradeSkillNumReagents(i) do
					local reagentName, _, _, _ = GetTradeSkillReagentInfo(i, j)
					if (reagentName) then
						local itemlink = GetTradeSkillReagentItemLink(i, j)
						if (itemlink) then
							local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
							self:StoreSessionItem(itemID)
						end
					end
				end
			end
		end
	end
end

function InventoryLib:ScanCraft()
	if (CraftFrame ~= nil and CraftFrame:IsVisible() and (time() > self.localvars.framescan_last + 1)) then
		-- This prevents further update events from being handled if we're already processing one.
		-- This is done to prevent the game from freezing under certain conditions.
		self.localvars.framescan_last = time()
		for i=1, GetNumCrafts() do
			local _, type, _, _ = GetCraftInfo(i)
			if (type ~= "header") then
				local itemlink = GetCraftItemLink(i)
				if (itemlink) then
					local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
					self:StoreSessionItem(itemID)
				end
				for j=1, GetCraftNumReagents(i) do
					local reagentName, _, _, _ = GetCraftReagentInfo(i, j)
					if (reagentName) then
						local itemlink = GetCraftReagentItemLink(i, j)
						if (itemlink) then
							local _, _, itemID = string.find(itemlink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r")
							self:StoreSessionItem(itemID)
						end
					end
				end
			end
		end
	end
end

-- -----
-- the following functions provide some tools used above
-- -----
function InventoryLib:link2baseid(link)
	if link then
		local _, _, baseid = string.find(link, "^(%d+)$")
		if not baseid then
			_, _, baseid = string.find(link, "item:(%d+):%d+:%d+:%d+");
		end
		return baseid
	end
	return
end

-- -----
-- lib stuff
-- -----

local function activate(self, oldLib, oldDeactivate)
	InventoryLib = self
	if oldLib then
		self.localvars = oldLib.localvars

		oldlib:CancelAllScheduledEvents()
		oldlib:UnregisterAllEvents()
	else
		self.localvars = {
			["framescan_last"]			= 0,			-- timestamp of latest frame scan (only one per second done)
			["inspected_lastclean"]		= 0,			-- timestamp of the last pruning of scanned units
			["inspected_units"]			= {},			-- list of latest scanned units
			["items"]					= {},			-- list of all known items keyed by id, true if session valid
			["items_scan_last"]			= 0,			-- id of the last scanned item; will scan consts.itemscancount items every second
			["items_new"]				= {},			-- list of new items seen this session, used for propagation
			["items_propagated"]		= 0,			-- id of the last nonnew item propagated
			["items_rescan"]			= {},			-- list of valid items not found in cache, yet
			["types_tree"]				= {},
			["types_base"]				= {},
			["types_sub"]				= {},
			["types_slot"]				= {},
		}
	end

	self.consts = {
		-- list of all slots scanned on inspect
		["InventorySlotName"] = {
			"HeadSlot",
			"NeckSlot",
			"ShoulderSlot",
			"BackSlot",
			"ChestSlot",
			"ShirtSlot",
			"TabardSlot",
			"WristSlot",
			"HandsSlot",
			"WaistSlot",
			"LegsSlot",
			"FeetSlot",
			"Finger0Slot",
			"Finger1Slot",
			"Trinket0Slot",
			"Trinket1Slot",
			"MainHandSlot",
			"SecondaryHandSlot",
			"RangedSlot",
			"AmmoSlot",
			"Bag0Slot",
			"Bag1Slot",
			"Bag2Slot",
			"Bag3Slot",
		},
		["itemscancount"] = 100,
		["maxid"] = 40000,
		["RealmName"] = GetRealmName()
	}

	self.locals = {
		["INVTYPE_OTHER"] = "Other",
	}

	if ( GetLocale() == "deDE" ) then
		self.locals.INVTYPE_OTHER = "Sonstiges"
	end

	if oldDeactivate then
		oldDeactivate(oldLib)
	end
end

local function external(self, major, instance)
	if major == "AceEvent-2.0" then
		AceEvent = instance
		AceEvent:embed(self)
		self:UnregisterAllEvents()
		self:CancelAllScheduledEvents()
		if AceEvent:IsFullyInitialized() then
			self:AceEvent_FullyInitialized()
		else
			self:RegisterEvent("AceEvent_FullyInitialized", "AceEvent_FullyInitialized", true)
		end		
	end
end

-- -----
-- events
-- -----

function InventoryLib:AceEvent_FullyInitialized()
	self:RegisterEvent("AUCTION_ITEM_LIST_UPDATE", "Event_Auction")
	self:RegisterEvent("BANKFRAME_OPENED", "Event_Bank")
	self:RegisterEvent("MERCHANT_SHOW", "Event_Merchant")
	self:RegisterEvent("MERCHANT_UPDATE", "Event_Merchant")
	self:RegisterEvent("TRADE_SKILL_SHOW", "Event_Tradeskill")
	self:RegisterEvent("TRADE_SKILL_UPDATE", "Event_Tradeskill")
	self:RegisterEvent("CRAFT_SHOW", "Event_Craft")
	self:RegisterEvent("CRAFT_UPDATE", "Event_Craft")

	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	self:RegisterEvent("PLAYER_TARGET_CHANGED")
	self:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
end

function InventoryLib:Event_Auction()
	self:ScanAuctionPage()
end

function InventoryLib:Event_Bank()
	self:Inspect("player")
	self:ScanBags()
	self:ScanBank()
end

function InventoryLib:Event_Merchant()
	self:ScanMerchant()
end

function InventoryLib:Event_Tradeskill()
	self:ScanTradeSkill()
end

function InventoryLib:Event_Craft()
	self:ScanCraft()
end

function InventoryLib:PLAYER_ENTERING_WORLD()
	self:Inspect("player")
	self:ScanBags()
end

function InventoryLib:PLAYER_TARGET_CHANGED()
	if UnitExists("target") and UnitIsPlayer("target") then
		self:Inspect("target")
	end
end

function InventoryLib:UPDATE_MOUSEOVER_UNIT()
	if UnitExists("mouseover") and UnitIsPlayer("mouseover") then
		self:Inspect("mouseover")
	end
end

-- -----
-- load the lib
-- -----
AceLibrary:Register(InventoryLib, MAJOR_VERSION, MINOR_VERSION, activate, nil, external)
